/*
 * @(#)FileSystemView.java	1.46 04/04/27
 *
 * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package dormouse.vfs.ui;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.UIManager;

import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.VFS;

import sun.awt.shell.ShellFolder;

/**
 * FileSystemView is JFileChooser's gateway to the file system. Since the JDK1.1
 * FileObject API doesn't allow access to such information as root partitions,
 * file type information, or hidden file bits, this class is designed to intuit
 * as much OS-specific file system information as possible.
 * 
 * <p>
 * 
 * Java Licensees may want to provide a different implementation of
 * FileSystemView to better handle a given operating system.
 * 
 * @version 1.46 04/27/04
 * @author Jeff Dinkins
 */

// PENDING(jeff) - need to provide a specification for
// how Mac/OS2/BeOS/etc file systems can modify FileSystemView
// to handle their particular type of file system.
public abstract class FileSystemView {

	static FileSystemView windowsFileSystemView = null;

	static FileSystemView unixFileSystemView = null;

	// static FileSystemView macFileSystemView = null;
	static FileSystemView genericFileSystemView = null;

	static boolean useSystemExtensionsHiding = false;

	static final String SEPARATOR = System.getProperty("file.separator");

	public static FileSystemView getFileSystemView() {
		useSystemExtensionsHiding = UIManager.getDefaults().getBoolean(
				"FileChooser.useSystemExtensionHiding");
		UIManager.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				if (e.getPropertyName().equals("lookAndFeel")) {
					useSystemExtensionsHiding = UIManager.getDefaults()
							.getBoolean("FileChooser.useSystemExtensionHiding");
				}
			}
		});

		if (SEPARATOR.equals("\\")) {
			if (windowsFileSystemView == null) {
				windowsFileSystemView = new WindowsFileSystemView();
			}
			return windowsFileSystemView;
		}

		if (SEPARATOR.equals("/")) {
			if (unixFileSystemView == null) {
				unixFileSystemView = new UnixFileSystemView();
			}
			return unixFileSystemView;
		}

		// if(FileObject.separatorChar == ':') {
		// if(macFileSystemView == null) {
		// macFileSystemView = new MacFileSystemView();
		// }
		// return macFileSystemView;
		// }

		if (genericFileSystemView == null) {
			genericFileSystemView = new GenericFileSystemView();
		}
		return genericFileSystemView;
	}

	/**
	 * Determines if the given file is a root in the navigatable tree(s).
	 * Examples: Windows 98 has one root, the Desktop folder. DOS has one root
	 * per drive letter, <code>C:\</code>, <code>D:\</code>, etc. Unix has
	 * one root, the <code>"/"</code> directory.
	 * 
	 * The default implementation gets information from the
	 * <code>ShellFolder</code> class.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object representing a directory
	 * @return <code>true</code> if <code>f</code> is a root in the
	 *         navigatable tree.
	 * @see #isFileSystemRoot
	 */
	public boolean isRoot(FileObject f) {

		try {
			if (f == null || f.getChildren() != null
					|| f.getChildren().length != 0) {
				return false;
			}
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		FileObject[] roots = getRoots();
		for (int i = 0; i < roots.length; i++) {
			if (roots[i].equals(f)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if the file (directory) can be visited. Returns false if the
	 * directory cannot be traversed.
	 * 
	 * @param f
	 *            the <code>FileObject</code>
	 * @return <code>true</code> if the file/directory can be traversed,
	 *         otherwise <code>false</code>
	 * @see JFileChooser#isTraversable
	 * @see FileView#isTraversable
	 */
	public Boolean isTraversable(FileObject f) {

		Boolean value = null;
		try {
			value = Boolean.valueOf(f.getType() == FileType.FOLDER);
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return value;
	}

	/**
	 * Name of a file, directory, or folder as it would be displayed in a system
	 * file browser. Example from Windows: the "M:\" directory displays as
	 * "CD-ROM (M:)"
	 * 
	 * The default implementation gets information from the ShellFolder class.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object
	 * @return the file name as it would be displayed by a native file chooser
	 * @see JFileChooser#getName
	 */
	public String getSystemDisplayName(FileObject f) {
		String name = null; 
		if (f != null) {
			name = f.getName().toString();
			try {
				if (!name.equals("..")
						&& !name.equals(".")
						&& (useSystemExtensionsHiding || !isFileSystem(f) || isFileSystemRoot(f))
						&& ((f instanceof ShellFolder) || f.getContent()
								.getFile().exists())) {

					name = f.getName().getBaseName();
					if (name == null || name.length() == 0) {
						name = f.getName().getPath(); // e.g. "/"
					}
				}
			} catch (FileSystemException e) {
				e.printStackTrace();
			}
		}
		return name;
	}

	/**
	 * Type description for a file, directory, or folder as it would be
	 * displayed in a system file browser. Example from Windows: the "Desktop"
	 * folder is desribed as "Desktop".
	 * 
	 * Override for platforms with native ShellFolder implementations.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object
	 * @return the file type description as it would be displayed by a native
	 *         file chooser or null if no native information is available.
	 * @see JFileChooser#getTypeDescription
	 */
	public String getSystemTypeDescription(FileObject f) {
		return null;
	}

	/**
	 * Icon for a file, directory, or folder as it would be displayed in a
	 * system file browser. Example from Windows: the "M:\" directory displays a
	 * CD-ROM icon.
	 * 
	 * The default implementation gets information from the ShellFolder class.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object
	 * @return an icon as it would be displayed by a native file chooser
	 * @see JFileChooser#getIcon
	 */
	public Icon getSystemIcon(FileObject f) {
		Icon icon = null;
		if (f != null) {
			
			try {
				icon = UIManager
						.getIcon((f.getType() == FileType.FOLDER) ? "FileView.directoryIcon"
								: "FileView.fileIcon");
			} catch (FileSystemException e) {
				e.printStackTrace();
			}
			
		}
		return icon;
	}

	/**
	 * On Windows, a file can appear in multiple folders, other than its parent
	 * directory in the filesystem. Folder could for example be the "Desktop"
	 * folder which is not the same as file.getParentFile().
	 * 
	 * @param folder
	 *            a <code>FileObject</code> object repesenting a directory or
	 *            special folder
	 * @param file
	 *            a <code>FileObject</code> object
	 * @return <code>true</code> if <code>folder</code> is a directory or
	 *         special folder and contains <code>file</code>.
	 */
	public boolean isParent(FileObject folder, FileObject file) {
		boolean isParent = false;
		String parentName = null;
		try {
			parentName = file.getParent().getName().toString();
		} catch (FileSystemException e) {
			e.printStackTrace();
		}
		String folderName = folder.getName().toString();
		return (folderName.equals(parentName));
		
	}

	/**
	 * 
	 * @param parent
	 *            a <code>FileObject</code> object repesenting a directory or
	 *            special folder
	 * @param fileName
	 *            a name of a file or folder which exists in <code>parent</code>
	 * @return a FileObject object. This is normally constructed with <code>new
	 * FileObject(parent, fileName)</code>
	 *         except when parent and child are both special folders, in which
	 *         case the <code>FileObject</code> is a wrapper containing a
	 *         <code>ShellFolder</code> object.
	 */
	public FileObject getChild(FileObject parent, String fileName) {
		if (parent instanceof ShellFolder) {
			FileObject[] children = getFiles(parent, false);
			for (int i = 0; i < children.length; i++) {
				if (children[i].getName().getBaseName().equals(fileName)) {
					return children[i];
				}
			}
		}
		return createFileObject(parent, fileName);
	}

	/**
	 * Checks if <code>f</code> represents a real directory or file as opposed
	 * to a special folder such as <code>"Desktop"</code>. Used by UI classes
	 * to decide if a folder is selectable when doing directory choosing.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object
	 * @return <code>true</code> if <code>f</code> is a real file or
	 *         directory.
	 */
	public boolean isFileSystem(FileObject f) {
		if (f instanceof ShellFolder) {
			ShellFolder sf = (ShellFolder) f;
			// Shortcuts to directories are treated as not being file system
			// objects,
			// so that they are never returned by JFileChooser.
			return sf.isFileSystem() && !(sf.isLink() && sf.isDirectory());
		} else {
			return true;
		}
	}

	/**
	 * Creates a new folder with a default folder name.
	 */
	public abstract FileObject createNewFolder(FileObject containingDir)
			throws IOException;

	/**
	 * Returns whether a file is hidden or not.
	 */
	public boolean isHiddenFile(FileObject f) {
		boolean isHidden = false;
		try {
			isHidden = f.isHidden();
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return isHidden;
	}

	/**
	 * Is dir the root of a tree in the file system, such as a drive or
	 * partition. Example: Returns true for "C:\" on Windows 98.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object representing a directory
	 * @return <code>true</code> if <code>f</code> is a root of a filesystem
	 * @see #isRoot
	 */
	public boolean isFileSystemRoot(FileObject dir) {
		boolean isRoot = false;
		try {
			isRoot =  dir.getFileSystem().getRoot().equals(dir);
		} catch (FileSystemException e) {
			e.printStackTrace();
		}
		return isRoot;
	}

	/**
	 * Used by UI classes to decide whether to display a special icon for drives
	 * or partitions, e.g. a "hard disk" icon.
	 * 
	 * The default implementation has no way of knowing, so always returns
	 * false.
	 * 
	 * @param dir
	 *            a directory
	 * @return <code>false</code> always
	 */
	public boolean isDrive(FileObject dir) {
		return false;
	}

	/**
	 * Used by UI classes to decide whether to display a special icon for a
	 * floppy disk. Implies isDrive(dir).
	 * 
	 * The default implementation has no way of knowing, so always returns
	 * false.
	 * 
	 * @param dir
	 *            a directory
	 * @return <code>false</code> always
	 */
	public boolean isFloppyDrive(FileObject dir) {
		return false;
	}

	/**
	 * Used by UI classes to decide whether to display a special icon for a
	 * computer node, e.g. "My Computer" or a network server.
	 * 
	 * The default implementation has no way of knowing, so always returns
	 * false.
	 * 
	 * @param dir
	 *            a directory
	 * @return <code>false</code> always
	 */
	public boolean isComputerNode(FileObject dir) {
		return false;
		// return ShellFolder.isComputerNode(dir);
	}

	/**
	 * Returns all root partitions on this system. For example, on Windows, this
	 * would be the "Desktop" folder, while on DOS this would be the A: through
	 * Z: drives.
	 */
	public FileObject[] getRoots() {
		// Don't cache this array, because filesystem might change
		FileObject[] roots = (FileObject[]) ShellFolder.get("roots");

		for (int i = 0; i < roots.length; i++) {
			if (isFileSystemRoot(roots[i])) {
				roots[i] = createFileSystemRoot(roots[i]);
			}
		}
		return roots;
	}

	// Providing default implementations for the remaining methods
	// because most OS file systems will likely be able to use this
	// code. If a given OS can't, override these methods in its
	// implementation.

	public FileObject getHomeDirectory() {
		return createFileObject(System.getProperty("user.home"));
	}

	/**
	 * Return the user's default starting directory for the file chooser.
	 * 
	 * @return a <code>FileObject</code> object representing the default
	 *         starting folder
	 */
	public FileObject getDefaultDirectory() {
		FileObject f = (FileObject) ShellFolder.get("fileChooserDefaultFolder");
		if (isFileSystemRoot(f)) {
			f = createFileSystemRoot(f);
		}
		return f;
	}

	/**
	 * Returns a FileObject object constructed in dir from the given filename.
	 */
	public FileObject createFileObject(FileObject dir, String filename) {
		FileObject file = null;
		try {
			if (dir == null) {

				file = VFS.getManager().resolveFile(filename);

			} else {
				file = VFS.getManager().resolveFile(dir, filename);
			}
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return file;
	}

	/**
	 * Returns a FileObject object constructed from the given path string.
	 */
	public FileObject createFileObject(String path) {
		FileObject f = null;
		try {
			f = VFS.getManager().resolveFile(path);
		} catch (FileSystemException e) {
			e.printStackTrace();
		}
		if (isFileSystemRoot(f)) {
			f = createFileSystemRoot(f);
		}
		return f;
	}

	/**
	 * Gets the list of shown (i.e. not hidden) files.
	 */
	public FileObject[] getFiles(FileObject dir, boolean useFileHiding) {
		Vector files = new Vector();
		FileObject[] children;
		FileObject[] children2 = null;
		try {
			children = dir.getChildren();

			for (int i = 0; i < children.length; i++) {
				if (!children[i].isHidden()) {
					files.add(children);
				}
			}
			children2 = new FileObject[files.size()];

		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return (FileObject[]) files.toArray(children2);

	}

	/**
	 * Returns the parent directory of <code>dir</code>.
	 * 
	 * @param dir
	 *            the <code>FileObject</code> being queried
	 * @return the parent directory of <code>dir</code>, or <code>null</code>
	 *         if <code>dir</code> is <code>null</code>
	 */
	public FileObject getParentDirectory(FileObject dir) {
		FileObject parent = null;
		try {
			parent = dir.getParent();
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return parent;
	}



	/**
	 * Creates a new <code>FileObject</code> object for <code>f</code> with
	 * correct behavior for a file system root directory.
	 * 
	 * @param f
	 *            a <code>FileObject</code> object representing a file system
	 *            root directory, for example "/" on Unix or "C:\" on Windows.
	 * @return a new <code>FileObject</code> object
	 */
	protected FileObject createFileSystemRoot(FileObject f) {
		FileObject fsRoot = null;
		try {
			fsRoot = f.getFileSystem().getRoot();
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return fsRoot;

	}

}

/**
 * FileSystemView that handles some specific unix-isms.
 */
class UnixFileSystemView extends FileSystemView {

	private static final String newFolderString = UIManager
			.getString("FileChooser.other.newFolder");

	private static final String newFolderNextString = UIManager
			.getString("FileChooser.other.newFolder.subsequent");

	/**
	 * Creates a new folder with a default folder name.
	 */
	public FileObject createNewFolder(FileObject containingDir)
			throws IOException {
		if (containingDir == null) {
			throw new IOException("Containing directory is null:");
		}
		FileObject newFolder = null;
		// Unix - using OpenWindows' default folder name. Can't find one for
		// Motif/CDE.
		newFolder = createFileObject(containingDir, newFolderString);
		int i = 1;
		while (newFolder.exists() && (i < 100)) {
			newFolder = createFileObject(containingDir, MessageFormat.format(
					newFolderNextString, new Object[] { Integer.valueOf(i) }));
			i++;
		}

		if (newFolder.exists()) {
			throw new IOException("Directory already exists:"
					+ newFolder.getName().getPath());
		} else {
			newFolder.createFolder();
		}

		return newFolder;
	}

	public boolean isFileSystemRoot(FileObject dir) {
		return (dir != null && dir.getName().getPath().equals("/"));
	}

	public boolean isDrive(FileObject dir) {
		if (isFloppyDrive(dir)) {
			return true;
		} else {
			return false;
		}
	}

	public boolean isFloppyDrive(FileObject dir) {
		// Could be looking at the path for Solaris, but wouldn't be reliable.
		// For example:
		// return (dir != null &&
		// dir.getAbsolutePath().toLowerCase().startsWith("/floppy"));
		return false;
	}

	public boolean isComputerNode(FileObject dir) {
		if (dir != null) {
			String parent = null;
			try {
				parent = dir.getParent().getName().toString();
			} catch (FileSystemException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			if (parent != null && parent.equals("/net")) {
				return true;
			}
		}
		return false;
	}
}

/**
 * FileSystemView that handles some specific windows concepts.
 */
class WindowsFileSystemView extends FileSystemView {

	private static final String newFolderString = UIManager
			.getString("FileChooser.win32.newFolder");

	private static final String newFolderNextString = UIManager
			.getString("FileChooser.win32.newFolder.subsequent");

	public Boolean isTraversable(FileObject f) {

		Boolean isTrav = null;
		try {
			isTrav = Boolean.valueOf(isFileSystemRoot(f) || isComputerNode(f)
					|| f.getType() == FileType.FOLDER);
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return isTrav;
	}

	public FileObject getChild(FileObject parent, String fileName) {
		if (fileName.startsWith("\\") && !(fileName.startsWith("\\\\"))
				&& isFileSystem(parent)) {

			// Path is relative to the root of parent's drive
			String path = parent.getName().getPath();
			if (path.length() >= 2 && path.charAt(1) == ':'
					&& Character.isLetter(path.charAt(0))) {

				return createFileObject(path.substring(0, 2) + fileName);
			}
		}
		return super.getChild(parent, fileName);
	}

	/**
	 * Type description for a file, directory, or folder as it would be
	 * displayed in a system file browser. Example from Windows: the "Desktop"
	 * folder is desribed as "Desktop".
	 * 
	 * The Windows implementation gets information from the ShellFolder class.
	 */
	public String getSystemTypeDescription(FileObject f) {
		String desc = "";
		FileType type = null;
		try {
			type = f.getType();
			if (type == FileType.FILE){
				desc = "File";
			}else if (type == FileType.FOLDER){
				desc = "Folder";
			}
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		
		return desc;
	}

	/**
	 * @return the Desktop folder.
	 */
	public FileObject getHomeDirectory() {
		return getRoots()[0];
	}

	/**
	 * Creates a new folder with a default folder name.
	 */
	public FileObject createNewFolder(FileObject containingDir)
			throws IOException {
		if (containingDir == null) {
			throw new IOException("Containing directory is null:");
		}
		FileObject newFolder = null;
		// Using NT's default folder name
		newFolder = createFileObject(containingDir, newFolderString);
		int i = 2;
		while (newFolder.exists() && (i < 100)) {
			newFolder = createFileObject(containingDir, MessageFormat.format(
					newFolderNextString, new Object[] { Integer.valueOf(i) }));
			i++;
		}

		if (newFolder.exists()) {
			throw new IOException("Directory already exists:"
					+ newFolder.getName().getPath());
		} else {
			newFolder.createFolder();
		}

		return newFolder;
	}

	public boolean isDrive(FileObject dir) {
		return isFileSystemRoot(dir);
	}

	public boolean isFloppyDrive(FileObject dir) {
		String path = dir.getName().getPath();
		return (path != null && (path.equals("A:\\") || path.equals("B:\\")));
	}

	/**
	 * Returns a FileObject object constructed from the given path string.
	 */
	public FileObject createFileObject(String path) {
		// Check for missing backslash after drive letter such as "C:" or
		// "C:filename"
		if (path.length() >= 2 && path.charAt(1) == ':'
				&& Character.isLetter(path.charAt(0))) {
			if (path.length() == 2) {
				path += "\\";
			} else if (path.charAt(2) != '\\') {
				path = path.substring(0, 2) + "\\" + path.substring(2);
			}
		}
		return super.createFileObject(path);
	}

	protected FileObject createFileSystemRoot(FileObject f) {
		// Problem: Removable drives on Windows return false on f.exists()
		// Workaround: Override exists() to always return true.
		FileObject fsRoot = null;
		try {
			fsRoot = VFS.getManager().createFileSystem(f);
		} catch (FileSystemException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return fsRoot;

	}

}

/**
 * Fallthrough FileSystemView in case we can't determine the OS.
 */
class GenericFileSystemView extends FileSystemView {

	private static final String newFolderString = UIManager
			.getString("FileChooser.other.newFolder");

	/**
	 * Creates a new folder with a default folder name.
	 */
	public FileObject createNewFolder(FileObject containingDir)
			throws IOException {
		if (containingDir == null) {
			throw new IOException("Containing directory is null:");
		}
		FileObject newFolder = null;
		// Using NT's default folder name
		newFolder = createFileObject(containingDir, newFolderString);

		if (newFolder.exists()) {
			throw new IOException("Directory already exists:"
					+ newFolder.getName().getPath());
		} else {
			newFolder.createFolder();
		}

		return newFolder;
	}

}
